package cn.lg.soar.system.account.service.impl;

import cn.lg.soar.common.data.msg.I18n;
import cn.lg.soar.common.exceptions.BaseAException;
import cn.lg.soar.common.exceptions.BaseCException;
import cn.lg.soar.common.exceptions.BaseException;
import cn.lg.soar.common.util.AssertUtil;
import cn.lg.soar.common.util.UrlUtils;
import cn.lg.soar.common.util.data.DataUtil;
import cn.lg.soar.common.util.data.StringUtil;
import cn.lg.soar.core.util.InnerSerializeUtils;
import cn.lg.soar.mvc.util.RequestUtils;
import cn.lg.soar.system.account.model.*;
import cn.lg.soar.system.account.service.ILoginService;
import cn.lg.soar.system.account.service.ISsoClientService;
import cn.lg.soar.system.api.constant.ModuleConst;
import cn.lg.soar.system.auth.entity.User;
import cn.lg.soar.system.auth.service.IUserService;
import cn.lg.soar.system.config.SsoClientProps;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.client.RestTemplate;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * 单点登录客户端
 * @author luguoxiang
 * @date 2022/5/28
 * 开源项目：https://gitee.com/lgx1992/lg-soar 求star！请给我star！请帮我点个star！
 */
@ConditionalOnProperty(name = "lg.sso.client.enabled", havingValue = "true")
public class SsoClientServiceImpl implements ISsoClientService {

    private static final Logger LOGGER = LoggerFactory.getLogger(SsoClientServiceImpl.class);

    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private IUserService userService;
    @Autowired
    private ILoginService loginService;

    private final Map<String, SsoClientProps.App> appMap;
    private final String logoutNotifyUrl;
    private final String homeUrl;

    private final static String ONLINE_URI = ModuleConst.MODULE_NAME + "/sso/server/online";

    public SsoClientServiceImpl(SsoClientProps props) {
        Map<String, SsoClientProps.App> appMap = new HashMap<>();
        logoutNotifyUrl = props.getLogoutNotifyUrl();
        homeUrl = props.getHomeUrl();
        this.appMap = appMap;
        if (Boolean.TRUE.equals(props.getEnabled())) {
            SsoClientProps.App[] apps = props.getApps();
            for (int i = 0; i < apps.length; i++) {
                SsoClientProps.App app = apps[i];
                appMap.put(app.getServerId(), app);
            }
        }
    }

    @Cacheable(value = "sso:server", key = "#serverId")
    @Override
    public Config getConfig(String serverId) {
        SsoClientProps.App app;
        if (DataUtil.isEmpty(serverId)) {
            AssertUtil.notEmpty(appMap.size() == 1, "请指定 serverId");
            Collection<SsoClientProps.App> values = appMap.values();
            app = null;
            for (SsoClientProps.App value : values) {
                app = value;
            }
        } else {
            app = appMap.get(serverId);
        }
        if (app == null) {
            LOGGER.warn("不存在的 serverId: " + serverId);
            throw new BaseAException(6, I18n.build("ticket 无效"));
        }
        String serverHost = app.getServerHost();
        SsoClientInfo ssoClientInfo = new SsoClientInfo();
        ssoClientInfo.setLogoutNotifyUrl(logoutNotifyUrl);
        ssoClientInfo.setHomeUrl(homeUrl);
        ssoClientInfo.setServerId(serverId);
        ssoClientInfo.setClientId(app.getClientId());
        ssoClientInfo.signature(app.getSecret());

        // 请求服务端，获取访问token（上线）
        SsoServerInfo serverInfo = post(serverHost + ONLINE_URI, ssoClientInfo, SsoServerInfo.class);
        if (serverInfo.verification(app.getSecret())) {
            Config config = new Config();
            config.setValidationUri(serverHost + serverInfo.getValidationUri());
            config.setLogoutNotifyUri(serverHost + serverInfo.getLogoutNotifyUri());
            config.setOfflineUri(serverHost + serverInfo.getOfflineUri());
            config.setSsoToken(serverInfo.getSsoToken());
            config.setClientId(app.getClientId());
            config.setSecret(app.getSecret());
            config.setServerHost(serverHost);
            return config;
        }
        LOGGER.warn("验签失败：" + serverInfo);
        throw new BaseCException(1, I18n.build("验签失败"));
    }

    @Override
    public String validation(String serverId, String ticket) {
        Config config = proxy().getConfig(serverId == null ? "" : serverId);
        SsoValidation ssoValidation = new SsoValidation();
        ssoValidation.setTicket(ticket);
        ssoValidation.setSsoToken(config.getSsoToken());
        SsoUser ssoUser = get(config.getValidationUri(), ssoValidation, SsoUser.class);
        I18n msg = I18n.build("单点登录失败");
        AssertUtil.notNull(ssoUser, msg);
        AssertUtil.notEmpty(ssoUser.getUsername(), msg);
        User user = userService.getByUsername(ssoUser.getUsername());
        if (user == null) {
            // 没有用户同步
            user = new User();
            user.setUsername(ssoUser.getUsername());
            user.setNickname(ssoUser.getNickname());
            user.setAvatar(ssoUser.getAvatar());
            user.setUserType(ssoUser.getType());
            user.setPassword("sync user add");
            user.setSecret("sync user add");
            AssertUtil.isTrue(userService.save(user), "同步用户失败");
        }
        // 登录
        return loginService.login(user.getId(), Boolean.TRUE.equals(ssoUser.getRememberMe()));
    }

    @Override
    public void logout(SsoNotifyClientLogout ssoLogout) {
        Config config = proxy().getConfig(ssoLogout.getServerId());
        AssertUtil.isTrue(ssoLogout.verification(config.getSecret()), "验签失败");
        // 退出登录
        if (ssoLogout.getDeviceType() != null) {
            loginService.logout(ssoLogout.getUsername(), RequestUtils.toDeviceType(ssoLogout.getDeviceType()));
        } else {
            loginService.logout(ssoLogout.getUsername());
        }
    }

    @Override
    public void notifyServerLogout(String serverId, SsoUserBase user) {
        Config config = proxy().getConfig(serverId);
        SsoNotifyServerLogout notifyServer = new SsoNotifyServerLogout();
        notifyServer.setSsoToken(config.getSsoToken());
        notifyServer.setUsername(user.getUsername());
        notifyServer.setDeviceType(user.getDeviceType());
        // 通知服务端用户下线
        get(config.getLogoutNotifyUri(), notifyServer, null);
    }

    /**
     * get请求
     * @param url
     * @param params
     * @param tClass
     * @param <T>
     * @return
     */
    private <T>T get(String url, Object params, Class<T> tClass) {
        url = UrlUtils.handlerParams(url, params);
        String response = restTemplate.getForObject(url, String.class);
        return parseResponse(response, tClass);
    }

    /**
     * post请求
     * @param url
     * @param body
     * @param tClass
     * @param <T>
     * @return
     */
    private <T>T post(String url, Object body, Class<T> tClass) {
        String response = restTemplate.postForObject(url, body, String.class);
        return parseResponse(response, tClass);
    }

    /**
     * 解析响应
     * @param response
     * @param tClass
     * @param <T>
     * @return
     */
    private <T>T parseResponse(String response, Class<T> tClass) {
        if (StringUtil.isBlank(response)) {
            return null;
        }
        if (response.contains("\"success\":") && response.contains("\"ts\":")) {
            JSONObject jsonObject = InnerSerializeUtils.parseObject(response);
            if ("false".equals(jsonObject.getString("success"))) {
                throw new BaseException(
                        jsonObject.getString("code"),
                        I18n.build(jsonObject.getString("msg"))
                );
            }
            response = jsonObject.getString("data");
        }
        if (Void.class.isAssignableFrom(tClass)) {
            return null;
        }
        if (String.class.isAssignableFrom(tClass)) {
            return (T) response;
        }
        return InnerSerializeUtils.parseObject(response, tClass);
    }

}
